Skip to content

refac: Harmonize linopy operations with breaking convention#591

Draft
FBumann wants to merge 60 commits intomasterfrom
harmonize-linopy-operations-mixed
Draft

refac: Harmonize linopy operations with breaking convention#591
FBumann wants to merge 60 commits intomasterfrom
harmonize-linopy-operations-mixed

Conversation

@FBumann
Copy link
Collaborator

@FBumann FBumann commented Feb 20, 2026

Harmonize linopy arithmetic with legacy/v1 convention transition

This PR harmonizes coordinate alignment and NaN handling in linopy arithmetic, with a non-breaking transition layer.

linopy.options["arithmetic_convention"]

Two modes:

  • "legacy" (default) — reproduces current master behavior exactly. Emits LinopyDeprecationWarning on every legacy codepath.
  • "v1" — strict exact-join semantics: mismatched coordinates raise ValueError with helpful messages. NaN values propagate (no implicit fillna).
linopy.options["arithmetic_convention"] = "v1"

v1 behavior

Exact coordinate matching — Arithmetic operators (+, -, *, /) require matching coordinates on shared dimensions. Mismatched coordinates raise ValueError with suggestions:

x[i=0,1,2] + y[i=1,2,3]  # ValueError: use .add(y, join="inner")

Named methods with explicit join.add(), .sub(), .mul(), .div(), .le(), .ge(), .eq() accept join= parameter:

x.add(y, join="inner")      # intersection
x.add(y, join="outer")      # union with fill
x.add(y, join="left")       # keep x's coordinates
x.add(y, join="override")   # positional alignment

NaN propagation — NaN in constants propagates through arithmetic (no implicit fillna). Use .fillna() as an escape hatch:

x + data_with_nans.fillna(0)        # addition: fill with 0
x * scaling_factors.fillna(1)       # multiplication: fill with 1

Free broadcasting — Constants can introduce new dimensions. All algebraic laws hold.

merge() behavior

merge() enforces exact matching on shared user-dimension coordinates in v1 mode. Helper dims (_term, _factor) and the concat dim are excluded from this check, since they legitimately differ between expressions. The actual xr.concat uses join="outer".

In legacy mode, merge uses override when shared user dims have matching sizes, outer otherwise.

Legacy behavior (default, backward-compatible)

  • Size-aware alignment: override when sizes match, left-join otherwise
  • NaN as neutral element: filled with 0 (add/sub/mul) or 1 (div)
  • Constraint RHS: NaN means "no constraint", preserved through subtraction

Source changes

File Change
config.py LinopyDeprecationWarning, arithmetic_convention setting
expressions.py All arithmetic paths branch on convention; merge() pre-validates user-dim coords under v1; to_constraint has separate legacy/v1 paths; scalar fast path for mul/div
common.py align() reads convention (legacy→inner, v1→exact)
variables.py Scalar fast path in __mul__, explicit TypeError in __div__, .reindex() methods
monkey_patch_xarray.py DataArray/Dataset arithmetic with linopy types
model.py Convention-aware model methods

Test structure

  • v1 tests (test_linear_expression.py, test_constraints.py, test_algebraic_properties.py, test_common.py, test_typing.py): autouse fixture sets v1; test strict matching + explicit join escape hatches
  • Legacy tests (test_*_legacy.py): validate old behavior preserved under "legacy"
  • conftest.py: shared fixtures, lazy import linopy for CI compatibility

Rollout plan

  1. This PR: Default "legacy" — nothing breaks
  2. Downstream: Users opt in with linopy.options["arithmetic_convention"] = "v1"
  3. linopy v1: Flip default to "v1" and drop legacy mode

Open questions

  • from_tuples / linexpr() — Currently follows the global convention (legacy: override-or-outer, v1: exact). In practice, from_tuples is always called with same-coord variables (verified across linopy tests and PyPSA), so the convention choice doesn't matter for existing code. Open question: should from_tuples accept a join= parameter, or always use a fixed join mode regardless of convention? Modern PyPSA has largely moved to direct arithmetic (cost * var) over linexpr, so this is low-priority.
  • Pipe operator — Only linopy objects, or also constants? (follow-up PR)
  • Test deduplication — v1 and legacy test files share ~85% identical code. Could be parametrized or extracted into shared base classes (follow-up PR).

Test plan

  • All tests pass (2167 passed, mypy clean)
  • Legacy tests validate backward compatibility
  • v1 tests validate strict coordinate matching
  • CI doctest collection fixed (lazy import in conftest.py)
  • Documentation notebook (examples/arithmetic-convention.ipynb) with escape hatches including .fillna()

🤖 Generated with Claude Code

FabianHofmann and others added 19 commits February 9, 2026 14:28
Add le(), ge(), eq() methods to LinearExpression and Variable classes,
mirroring the pattern of add/sub/mul/div methods. These methods support
the join parameter for flexible coordinate alignment when creating constraints.
Consolidate repetitive alignment handling in _add_constant and
_apply_constant_op into a single _align_constant method. This
eliminates code duplication and makes the alignment behavior
(handling join parameter, fill_value, size-aware defaults) testable
and maintainable in one place.
numpy_to_dataarray no longer inflates ndim beyond arr.ndim, fixing
lower-dim numpy arrays as constraint RHS. Also reject higher-dim
constant arrays (numpy/pandas) consistently with DataArray behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use "exact" join for +/- (raises ValueError on mismatch), "inner" join
for *// (intersection), and "exact" for constraint DataArray RHS.
Named methods (.add(), .sub(), .mul(), .div(), .le(), .ge(), .eq())
accept explicit join= parameter as escape hatch.

- Remove shape-dependent "override" heuristic from merge() and
  _align_constant()
- Add join parameter support to to_constraint() for DataArray RHS
- Forbid extra dimensions on constraint RHS
- Update tests with structured raise-then-recover pattern
- Update coordinate-alignment notebook with examples and migration guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@FBumann
Copy link
Collaborator Author

FBumann commented Feb 20, 2026

@FabianHofmann Im quite happy with the notebook now. It showcases the convention and its consequences.
Tests need some work though. And migration as well.
Looking forward to your opinion on the convention

FBumann and others added 2 commits February 20, 2026 13:51
…ords. Here's what changed:

  - test_linear_expression_sum / test_linear_expression_sum_with_const: v.loc[:9].add(v.loc[10:], join="override") → v.loc[:9] + v.loc[10:].assign_coords(dim_2=v.loc[:9].coords["dim_2"])
  - test_add_join_override → test_add_positional_assign_coords: uses v + disjoint.assign_coords(...)
  - test_add_constant_join_override → test_add_constant_positional: now uses different coords [5,6,7] + assign_coords to make the test meaningful
  - test_same_shape_add_join_override → test_same_shape_add_assign_coords: uses + c.to_linexpr().assign_coords(...)
  - test_add_constant_override_positional → test_add_constant_positional_different_coords: expr + other.assign_coords(...)
  - test_sub_constant_override → test_sub_constant_positional: expr - other.assign_coords(...)
  - test_mul_constant_override_positional → test_mul_constant_positional: expr * other.assign_coords(...)
  - test_div_constant_override_positional → test_div_constant_positional: expr / other.assign_coords(...)
  - test_variable_mul_override → test_variable_mul_positional: a * other.assign_coords(...)
  - test_variable_div_override → test_variable_div_positional: a / other.assign_coords(...)
  - test_add_same_coords_all_joins: removed "override" from loop, added assign_coords variant
  - test_add_scalar_with_explicit_join → test_add_scalar: simplified to expr + 10
@FBumann
Copy link
Collaborator Author

FBumann commented Feb 27, 2026

The convention should be "exact" for all of +, -, *, /, with an additional check that neither side may introduce dimensions the other doesn't have — also for all operations.

Why "exact" instead of "inner" for * and /

"exact" still broadcasts freely over dimensions that only exist on one side — it only enforces strict matching on shared dimensions. So the common scaling pattern works fine:

cost = xr.DataArray([10, 20], coords=[("tech", ["wind", "solar"])])
capacity  # dims: (tech=["wind", "solar"], region=["A", "B"])

cost * capacity  # ✓ tech matches exactly, region broadcasts freely

"inner" is dangerous: if coords on a shared dimension don't match due to a typo or upstream change, it silently drops values. The explicit and safe way to subset before multiplying is:

capacity.sel(tech=["wind", "solar"]) * renewable_cost

No operation should introduce new dimensions

Neither side of any arithmetic operation should be allowed to introduce dimensions the other doesn't have. The same problem applies to + and - as to * and / — new dimensions silently expand the optimization problem in unintended ways:

cost_expr      # dims: (tech, time)
regional_expr  # dims: (tech, time, region)

cost_expr + regional_expr  # ✗ silently expands to (tech, time, region)

capacity  # dims: (tech, region, time)
risk      # dims: (tech, scenario)
risk * capacity  # ✗ silently expands to (tech, region, time, scenario)

An explicit pre-check on all operations:

asymmetric_dims = set(other.dims).symmetric_difference(set(self.dims))
if asymmetric_dims:
    raise ValueError(f"Operation introduces new dimensions: {asymmetric_dims}")

Summary

Operation Convention
+, -, *, / "exact" on shared dims; neither side may introduce dims the other doesn't have

@coroa
Copy link
Member

coroa commented Feb 27, 2026

The convention should be "exact" for all of +, -, *, /, with an additional check that neither side may introduce dimensions the other doesn't have — also for all operations.

Let's clearly differentiate between dimensions and labels.

labels

I agree with "exact" for labels by default, but we need an easy way to have inner or outer joining characteristics. I found the pyoframe conventions
strange at the beginning, but they grew on me:

x + y.keep_extras() to say that an outer join is in order and mismatches should fill with 0.

x + y.drop_extras() to say that you want an outer inner join.
x.drop_extras() + y does the same, though.

I have in a different project used | 0 to indicate keep_extras ie (x + y | 0).

dimensions

i am actually fond of the ability to auto broadcast over different dimensions. and would want to keep that (actually my main problem with pyoframe).

your first example actually implicitly assumes broadcasting.

@FBumann
Copy link
Collaborator Author

FBumann commented Feb 28, 2026

Dimensions and broadcasting

I agree that auto broadcasting is helpful in some cases.
I'm happy with allowing broadcasting of constants. We could allow this always...?
But I would enforce that the constant never has more dims than the variable/expression.
Or is there a use case for this?

So the full convention requires two separate things:
1. "exact" join — shared dims must have matching coords (xarray handles this)
2. Subset dim check — the constant side’s dims must be a subset of the variable/expression (custom pre-check needed)

labels

I'm not sure if I like this approach, as it's needs careful state management of the flags on expressions. The flag (keep or drop extras) needs to be handled.
I would rather enforce to reindex or fill data to the correct index.
I think aligning is the correct approach:

import linopy

# outer join — fill gaps with 0 before adding
x_aligned, y_aligned = linopy.align(x, y, join="outer", fill_value=0)
x_aligned + y_aligned

# inner join — drop non-matching coords before adding
x_aligned, y_aligned = linopy.align(x, y, join="inner")
x_aligned + y_aligned

Combining disjoint expressions would then still need the explicit methods though.
I'm interested about your take on this

@FBumann
Copy link
Collaborator Author

FBumann commented Feb 28, 2026

The proposed convention for all arithmetic operations in linopy:
1. "exact" join by default — shared coords must match exactly, raises on mismatch
2. Subset dim check — constants may introduce dimensions the variable/expression doesn’t have
3. No implicit inner join — use .sel() explicitly instead
4. Outer join with fill — use x + (y | 0) or .add(join="outer", fill_value=0)
The escape hatches in order of preference: .sel() for subsetting, | 0 for inline fill, named method .add(join=...) for everything else. No context manager needed.​​​​​​​​​​​​​​​​

I'm not sure how to implement the | operator yet. Might need some sort of flag/state for defered indexing

@FBumann
Copy link
Collaborator Author

FBumann commented Feb 28, 2026

I thought about the pipe operator:
I think it should only work with linopy internal types (Variables/expression), not constants (scalar, numpy, pandas, dataarray), as this would need monkey patching a lot and hard to get stable.

Would this be an issue for you?

FBumann and others added 6 commits March 9, 2026 21:02
Spec and tests for commutativity, associativity, distributivity,
identity, negation, and zero. Two known violations marked xfail:
associativity and distributivity with constants that introduce new dims.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Constants can now introduce new dimensions in arithmetic (+, -, *, /),
preserving all standard algebraic laws (associativity, distributivity).
The dim-subset check remains for constraint RHS to catch accidental
broadcasting. Default fill value for const changed from 0 to NaN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Constraint RHS can now introduce new dimensions, just like arithmetic.
For ==, broadcasting to incompatible values results in solver infeasibility.
For <=/>= it creates redundant but harmless constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pure xarray/pandas/numpy operations before entering linopy use their
own alignment rules. Document the risks and the xarray exact join
workaround.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document assign_coords (recommended) and join="override" for handling
operands with mismatched coordinate labels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@FBumann
Copy link
Collaborator Author

FBumann commented Mar 9, 2026

@coroa @FabianHofmann Im quite happy with this. Looking forward to your thoughts.
I updateted the PR description to the current state

- Remove dead check_common_keys_values function from common.py
- Remove as_dataarray from public API exports
- Remove redundant aligned_rhs = aligned_rhs in to_constraint
- Keep Variable.__mul__ delegating to linexpr (fast path bypasses exact join)
- Keep Variable.__div__ delegating to linexpr (error message preserved there)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@FabianHofmann
Copy link
Collaborator

Thanks for the comments to two. (I'm writing this the second time as the last post here did not make it.)

I agree that the equal comparison should be the way to go and I am fine with the general approach. Before going to the code, the question is how we do the transition. I would suggest the new convention should be opt-in until v1.0 where it is the standard (probably breaking and no backwards compat afterwards). For that we would need a linopy.options setting to enable the strict arithmetic convention.

Does that make sense @FBumann @coroa ?

About the pipe operator, I am open to that. but it's true that upstream library objects (pandas, xarray) should probably not be monkey-patched (we already do it and I would like to keep it to a minimum).

@FBumann
Copy link
Collaborator Author

FBumann commented Mar 10, 2026

@FabianHofmann Fully agreed on that.
New convention as opt in until v1.0.0, then drop the old one
Pipe operator will only work reliably with linopy objects (var + var)

@FBumann
Copy link
Collaborator Author

FBumann commented Mar 10, 2026

@FabianHofmann @coroa If you agree i merge #607 in here to get the full diff view and prepare a single PR

@FabianHofmann
Copy link
Collaborator

@FabianHofmann @coroa If you agree i merge #607 in here to get the full diff view and prepare a single PR

yes, please go ahead

FabianHofmann and others added 9 commits March 10, 2026 09:37
… as 'no constraint' in RHS

- Fill NaN with 0 (add/sub) or fill_value (mul/div) in _add_constant/_apply_constant_op
- Fill NaN coefficients with 0 in Variable.to_linexpr
- Restore NaN mask in to_constraint() so subset RHS still signals unconstrained positions
Move Self to TYPE_CHECKING block with typing_extensions fallback,
since typing.Self is only available in Python 3.11+.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace `join: str | None` with `join: JoinOptions | None` in expressions.py
  and variables.py to satisfy mypy's xr.align call-overload checking
- Add assertion for termination_condition in solvers.py SCIP solver
- Add type: ignore comments for xarray merge and reindex edge cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fill NaN values with neutral elements (0 for add, fill_value for mul/div)
  in constant arithmetic to prevent silent NaN propagation
- Fill NaN coefficients in Variable.to_linexpr
- Fix conftest.py to use lazy imports avoiding circular import issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Capture NaN positions in constant RHS before sub() fills them with 0,
then restore NaN afterward so they still signal unconstrained positions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- _align_constant: use override when sizes match, reindex_like otherwise
  (instead of strict exact join default)
- merge: restore check_common_keys_values override/outer logic
- to_constraint: restore SUPPORTED_CONSTANT_TYPES conversion with
  reindex_like and rhs_nan_mask preservation
- Sync test_linear_expression.py with origin/harmonize-linopy-operations
- Re-add check_common_keys_values to common.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Base automatically changed from harmonize-linopy-operations to master March 10, 2026 15:09
FBumann and others added 4 commits March 10, 2026 17:02
* Add legacy arithmetic join mode with deprecation warning for transition

- Add `options["arithmetic_join"]` setting (default: "legacy") to control
  coordinate alignment in arithmetic operations, merge, and constraints
- Legacy mode reproduces old behavior: override when shapes match, outer
  otherwise for merge; reindex_like for constants; inner for align()
- All legacy codepaths emit FutureWarning guiding users to opt in to "exact"
- Move shared test fixtures (m, x, y, z, v, u) to conftest.py
- Exact-behavior tests use autouse fixture to set arithmetic_join="exact"
- Legacy test files (test_*_legacy.py) validate old behavior is preserved
- All 2736 tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify global setting to 'legacy'/'v1', add LinopyDeprecationWarning

- Restrict options["arithmetic_join"] to {"legacy", "v1"} instead of
  exposing all xarray join values (explicit join= parameter still accepts any)
- "v1" maps to "exact" join internally
- Add LinopyDeprecationWarning class (subclass of FutureWarning) with
  centralized message including how to silence
- Export LinopyDeprecationWarning from linopy.__init__

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename arithmetic_join to arithmetic_convention, mention v1 removal

- Rename setting from 'arithmetic_join' to 'arithmetic_convention'
- Update deprecation message: "will be removed in linopy v1"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rename test fixtures from exact_join to v1_convention

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update legacy tests

* Merge harmonize-linopy-operations-mixed, restore NaN filling and align function

- Resolve merge conflicts keeping transition layer logic
- Restore NaN fillna(0) in _add_constant and _apply_constant_op
- Restore simple finisher-based align() function (fixes MultiIndex)
- Use check_common_keys_values in merge legacy path
- Update legacy test files to match origin/harmonize-linopy-operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Clean up obsolete code and fix convention-awareness in arithmetic

- Remove dead check_common_keys_values function from common.py
- Remove redundant default_join parameter from _align_constant, use
  options["arithmetic_convention"] directly
- Gate fillna(0) calls in _add_constant and _apply_constant_op behind
  legacy convention check so NaN values propagate correctly under v1
- Fix legacy to_constraint path to compute constraint RHS directly
  instead of routing through sub() which re-applies fillna
- Restore Variable.__mul__ scalar fast path via to_linexpr(other)
- Restore Variable.__div__ explicit TypeError for non-linear division
- Update v1 tests to expect ValueError on mismatched coords and test
  explicit join= escape hatches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix mypy errors and pytest importmode for CI

- Add return type annotations (Generator) to all v1_convention fixtures
- Add importmode = "importlib" to pytest config to fix import mismatch
  when linopy is installed from wheel and source dir is also present
- Use tuple literal in loop to fix arg-type error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: move import linopy to lazy in conftest.py

Top-level `import linopy` in conftest.py caused pytest to import the
package from site-packages before collecting doctests from the source
directory, triggering import file mismatch errors on all platforms.

Move the import inside fixture functions where it's actually needed.
Also revert the unnecessary test.yml and importmode changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…erations-mixed

# Conflicts:
#	linopy/expressions.py
#	linopy/variables.py
#	test/conftest.py
#	test/test_constraints.py
#	test/test_linear_expression.py
- merge() in v1 mode now pre-validates that shared user-dimension
  coordinates match exactly, then uses outer join for xr.concat
  (helper dims like _term/_factor are excluded from the check)
- Removed redundant pre-checks from LinearExpression.__add__ and
  QuadraticExpression.__add__ — merge handles enforcement now
- Added scalar fast path in _apply_constant_op (mul/div skip alignment)
- Wrapped AlignmentError import in try/except for xarray compat
- Fixed missing space in __div__ error message
- Added .fillna() as escape hatch option 5 in notebook
- Updated merge docstring with convention behavior summary
- Added explanatory comments (stacklevel, numpy_to_dataarray filtering)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FBumann and others added 2 commits March 10, 2026 21:38
* Deduplicate convention-specific test files into single files

Merge 4 pairs of v1/legacy test files into single files, eliminating
~2600 lines of duplicated test code. Convention-specific alignment tests
are kept in separate classes (V1/Legacy) with autouse fixtures, while
shared tests run under the module-level v1 convention.

- test_typing_legacy.py -> merged into test_typing.py (parametrized)
- test_common_legacy.py -> merged into test_common.py (legacy align test)
- test_constraints_legacy.py -> merged into test_constraints.py (legacy alignment class)
- test_linear_expression_legacy.py -> merged into test_linear_expression.py (legacy alignment + join classes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address PR review: consistency, dedup fixtures, missing test

- Add legacy_convention fixture to conftest.py; use it consistently
  instead of manual try/finally blocks (#1)
- Parametrize test_constant_with_extra_dims_broadcasts with convention
  fixture so it runs under both conventions (#2)
- Add missing test_quadratic_add_expr_join_inner to
  TestJoinParameterLegacy (#3)
- Extract shared fixtures into _CoordinateAlignmentFixtures and
  _ConstraintAlignmentFixtures mixin classes to eliminate fixture
  duplication between V1/Legacy alignment test classes (#4)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
)

* Restore master tests, add autouse convention fixture

- Restore test files to match master exactly (legacy behavior)
- Delete legacy duplicate test files
- Add autouse parametrized convention fixture: every test runs
  under both 'legacy' and 'v1' conventions by default
- Add legacy_convention/v1_convention opt-out fixtures for
  convention-specific tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Mark legacy-only tests, add v1 counterparts for differing behavior

Tests that differ between conventions are split:
- Legacy-only: marked with legacy_convention fixture (skipped under v1)
- V1-only: marked with v1_convention fixture (skipped under legacy)
- All other tests: run under both conventions via autouse fixture

Files changed:
- test_common.py: split test_align into legacy/v1 versions
- test_constraints.py: mark TestConstraintCoordinateAlignment as
  legacy-only, add TestConstraintCoordinateAlignmentV1, split
  higher-dim RHS tests
- test_linear_expression.py: mark TestCoordinateAlignment as
  legacy-only, add TestCoordinateAlignmentV1, split sum/join tests
- test_piecewise_constraints.py: mark legacy-only (implementation
  not yet v1-compatible)
- test_sos_reformulation.py: mark legacy-only (implementation
  not yet v1-compatible)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants